import SetupEnv from './common/setup-env.mdx';

# 与任意界面集成（预览特性）

从 Midscene v0.28.0 开始，我们推出了与任意界面集成的功能。定义符合 `AbstractInterface` 定义的界面控制器类，即可获得一个功能齐全的 Midscene Agent。

该功能的典型用途是构建一个针对你自己界面的 GUI 自动化 Agent，比如 IoT 设备、内部应用、车载显示器等。

在实现了 UI 操作的类之后，你可以获得以下特性：

- TypeScript 的 GUI 自动化 Agent SDK
- 用于调试的 Playground
- 通过 yaml 脚本控制界面
- Midscene Agent 的全部特性
- MCP 服务器 (仍在开发中...)

请注意：只有具备视觉定位（visual grounding）能力的模型才能用于操作 UI 界面。请阅读文档以[选择合适的模型](./choose-a-model)。


:::tip 预览功能说明
此功能仍在预览阶段，欢迎你在 [GitHub](https://github.com/web-infra-dev/midscene/issues) 上给我们提建议。
:::

## 演示和社区项目

我们已经为你准备了一个演示项目，帮助你学习如何定义自己的界面类。强烈建议你查看一下。

* [演示项目](https://github.com/web-infra-dev/midscene-example/tree/main/custom-interface) - 一个简单的演示项目，展示如何定义自己的界面类

* [Android (adb) Agent](https://github.com/web-infra-dev/midscene/blob/main/packages/android/src/device.ts) - 这是 Midscene Android (adb) Agent，同样依赖此特性实现

* [iOS (WebDriverAgent) Agent](https://github.com/web-infra-dev/midscene/blob/main/packages/ios/src/device.ts) - 这是 Midscene iOS (WebDriverAgent) Agent，同样依赖此特性实现

还有一些使用此功能的社区项目：

* [midscene-ios](https://github.com/lhuanyu/midscene-ios) - 使用 Midscene 驱动 "iPhone 镜像" 应用的项目


<SetupEnv />

## 实现你自己的界面类

### 关键概念

* `AbstractInterface` 类：一个预定义的抽象类，可以连接到 Midscene 智能体
* **动作空间**：描述可以在界面上执行的动作集合。这将影响 AI 模型如何规划和执行动作

### 步骤 1. 从 demo 项目开始

我们提供了一个演示项目，运行了本文档中的所有功能。这是最快的启动方式。

```bash
# 准备项目
git clone https://github.com/web-infra-dev/midscene-example.git
cd midscene-example/custom-interface
npm install
npm run build

# 运行演示
npm run demo
```

### 步骤 2. 实现你的界面类

定义一个继承 `AbstractInterface` 类的类，并实现所需的方法。

你可以从 [`./src/sample-device.ts`](https://github.com/web-infra-dev/midscene-example/blob/main/custom-interface/src/sample-device.ts) 文件中获取示例实现。让我们快速浏览一下。

```typescript
import type { DeviceAction, Size } from '@midscene/core';
import { getMidsceneLocationSchema, z } from '@midscene/core';
import {
  type AbstractInterface,
  defineAction,
  defineActionTap,
  defineActionInput,
  // ... 其他动作导入
} from '@midscene/core/device';

export interface SampleDeviceConfig {
  deviceName?: string;
  width?: number;
  height?: number;
  dpr?: number;
}

/**
 * SampleDevice - AbstractInterface 的模板实现
 */
export class SampleDevice implements AbstractInterface {
  interfaceType = 'sample-device';
  private config: Required<SampleDeviceConfig>;

  constructor(config: SampleDeviceConfig = {}) {
    this.config = {
      deviceName: config.deviceName || 'Sample Device',
      width: config.width || 1920,
      height: config.height || 1080,
      dpr: config.dpr || 1,
    };
  }

  /**
   * 必需：截取屏幕截图并返回 base64 字符串
   */
  async screenshotBase64(): Promise<string> {
    // TODO：实现实际的屏幕截图捕获
    console.log('📸 Taking screenshot...');
    return 'data:image/png;base64,...'; // 你的屏幕截图实现
  }

  /**
   * 必需：获取界面尺寸
   */
  async size(): Promise<Size> {
    return {
      width: this.config.width,
      height: this.config.height,
      dpr: this.config.dpr,
    };
  }

  /**
   * 必需：定义 AI 模型的可用动作
   */
  actionSpace(): DeviceAction[] {
    return [
      // 基础点击动作
      defineActionTap(async (param) => {
        // TODO：实现在 param.locate.center 坐标的点击
        await this.performTap(param.locate.center[0], param.locate.center[1]);
      }),

      // 文本输入动作  
      defineActionInput(async (param) => {
        // TODO：实现文本输入
        await this.performInput(param.locate.center[0], param.locate.center[1], param.value);
      }),

      // 自定义动作示例
      defineAction({
        name: 'CustomAction',
        description: '你的自定义设备特定动作',
        paramSchema: z.object({
          locate: getMidsceneLocationSchema(),
          // ... 自定义参数
        }),
        call: async (param) => {
          // TODO：实现自定义动作
        },
      }),
    ];
  }

  async destroy(): Promise<void> {
    // TODO：清理资源
  }

  // 私有实现方法
  private async performTap(x: number, y: number): Promise<void> {
    // TODO：你的实际点击实现
  }

  private async performInput(x: number, y: number, text: string): Promise<void> {
    // TODO：你的实际输入实现  
  }
}
```

需要实现的关键方法有：
- `screenshotBase64()`、`size()`：帮助 AI 模型获取界面上下文
- `actionSpace()`：一个由 `DeviceAction` 组成的数组，定义了在界面上可以执行的动作。AI 模型将使用这些动作来执行操作。Midscene 已为常见界面与设备提供了预定义动作空间，同时也支持定义任何自定义动作。

使用这些命令运行 Agent：

- `npm run build` 重新编译 Agent 代码
- `npm run demo` 使用 JavaScript 运行智能体
- `npm run demo:yaml` 使用 yaml 脚本运行智能体


### 步骤 3. 使用 Playground 测试 Agent

为 Agent 附加一个 Playground 服务，即可在浏览器中测试你的 Agent。

```ts 
import 'dotenv/config'; // 从 .env 文件里读取 Midscene 环境变量
import { playgroundForAgent } from '@midscene/playground';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 实例化 device 和 agent
const device = new SampleDevice();
await device.launch();
const agent = new Agent(device);

// 启动 playground
const server = await playgroundForAgent(agent).launch();

// 关闭 Playground
await sleep(10 * 60 * 1000);
await server.close();
console.log('Playground 已关闭！');
```

### 步骤 4. 测试 MCP 服务

（仍在开发中）

### 步骤 5. 发布 npm 包，让你的用户使用它

`./index.ts` 文件已经导出了你的 Agent 与界面类。现在可以发布到 npm。

在 `package.json` 文件中填写 `name` 和 `version`，然后运行以下命令：

```bash
npm publish
```

你的 npm 包的典型用法如下：

```typescript
import 'dotenv/config'; // 从 .env 文件里读取 Midscene 环境变量
import { playgroundForAgent } from '@midscene/playground';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 实例化 device 和 agent
const device = new SampleDevice();
await device.launch();
const agent = new Agent(device);

await agent.aiAction('click the button');
```

### 步骤 6. 在 Midscene CLI 和 YAML 脚本中调用你的类

编写一个包含 `interface` 字段的 yaml 脚本来调用你的类：

```yaml
interface:
  module: 'my-pkg-name'
  # export: 'MyDeviceClass' # 如果是具名导出，使用该字段

config:
  output: './data.json'
```

该配置等价于：

```typescript
import MyDeviceClass from 'my-pkg-name';
const device = new MyDeviceClass();
const agent = new Agent(device, {
  output: './data.json',
});
```

YAML 的其他字段与[自动化脚本](./automate-with-scripts-in-yaml.html)文档一致。

## API 参考

### `AbstractInterface` 类

```typescript
import { AbstractInterface } from '@midscene/core';
```

`AbstractInterface` 是智能体控制界面的关键类。

以下是你需要实现的必需方法：

- `interfaceType: string`：为界面定义一个名称，这不会提供给 AI 模型
- `screenshotBase64(): Promise<string>`：截取界面的屏幕截图并返回带有 `'data:image/` 前缀的 base64 字符串
- `size(): Promise<Size>`：界面的大小和 dpr，它是一个具有 `width`、`height` 和 `dpr` 属性的对象
- `actionSpace(): DeviceAction[] | Promise<DeviceAction[]>`：界面的动作空间，它是一个 `DeviceAction` 对象数组。在这里你可以使用预定义动作，或是自定义交互操作。

类型签名：

```ts
import type { DeviceAction, Size, UIContext } from '@midscene/core';
import type { ElementNode } from '@midscene/shared/extractor';

abstract class AbstractInterface {
  // 必选
  abstract interfaceType: string;
  abstract screenshotBase64(): Promise<string>;
  abstract size(): Promise<Size>;
  abstract actionSpace(): DeviceAction[] | Promise<DeviceAction[]>;

  // 可选：生命周期/钩子
  abstract destroy?(): Promise<void>;
  abstract describe?(): string;
  abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;
  abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;
}
```

以下是你可以实现的可选方法：

- `destroy?(): Promise<void>`：销毁
- `describe?(): string`：界面描述，这可能会用于报告和 Playground，但不会提供给 AI 模型
- `beforeInvokeAction?(actionName: string, param: any): Promise<void>`：在动作空间中调用动作之前的钩子函数
- `afterInvokeAction?(actionName: string, param: any): Promise<void>`：在调用动作之后的钩子函数

### 动作空间（Action Space）

动作空间是界面上可执行动作的集合。AI 模型将使用这些动作来执行操作。所有动作的描述和参数模式都会提供给 AI 模型。

为了帮助你轻松定义动作空间，Midscene 为最常见的界面和设备提供了一组预定义的动作，同时也支持定义任意自定义动作。

以下是如何导入工具来定义动作空间：

```typescript
import {
	type ActionTapParam,
	defineAction,
	defineActionTap,
} from "@midscene/core/device";
```

#### 预定义的动作

这些是最常见界面和设备的预定义动作空间。你可以通过实现动作的调用方法将它们暴露给定制化界面。

你可以在这些函数的类型定义中找到动作的参数。

* `defineActionTap()`：定义点击动作。这也是 `aiTap` 方法的调用函数。
* `defineActionDoubleClick()`：定义双击动作
* `defineActionInput()`：定义输入动作。这也是 `aiInput` 方法的调用函数。这也是 `aiInput` 方法的调用函数。
* `defineActionKeyboardPress()`：定义键盘按下动作。这也是 `aiKeyboardPress` 方法的调用函数。
* `defineActionScroll()`：定义滚动动作。这也是 `aiScroll` 方法的调用函数。
* `defineActionDragAndDrop()`：定义拖放动作
* `defineActionLongPress()`：定义长按动作
* `defineActionSwipe()`：定义滑动动作

#### 定义一个自定义动作

你可以使用 `defineAction()` 函数定义自己的动作。你也可以使用这种方式为 [PuppeteerAgent](./integrate-with-puppeteer)、[AgentOverChromeBridge](./bridge-mode-by-chrome-extension#constructor) 和 [AndroidAgent](./integrate-with-android) 定义更多动作。

API 签名：

```typescript
import { defineAction } from "@midscene/core/device";

defineAction(
  {
    name: string,
    description: string,
    paramSchema: z.ZodType<T>;
    call: (param: z.infer<z.ZodType<T>>) => Promise<void>;
  }
)
```

* `name`：动作的名称，AI 模型将使用此名称调用动作
* `description`：动作的描述，AI 模型将使用此描述来理解动作的作用。对于复杂动作，你可以在这里给出更详细的示例说明
* `paramSchema`：动作参数的 [Zod](https://www.npmjs.com/package/zod) 模式，AI 模型将根据此模式帮助填充参数
* `call`：调用动作的函数，你可以从符合 `paramSchema` 的 `param` 参数中获取参数


示例：

```typescript
defineAction({
  name: 'MyAction',
  description: 'My action',
  paramSchema: z.object({
    name: z.string(),
  }),
  call: async (param) => {
    console.log(param.name);
  },
});
```

如果你想要获取某个元素位置相关的参数，可以使用 `getMidsceneLocationSchema()` 函数获取特定的 zod 模式。

一个更复杂的示例，关于如何定义自定义动作：

```typescript
import { getMidsceneLocationSchema } from "@midscene/core/device";

defineAction({
  name: 'LaunchApp',
  description: '启动屏幕上的应用',
  paramSchema: z.object({
    name: z.string().describe('要启动的应用名称'),
    locate: getMidsceneLocationSchema().describe('要启动的应用图标'),
  }),
  call: async (param) => {
    console.log(`launching app: ${param.name}, ui located at: ${JSON.stringify(param.locate.center)}`);
  },
});
```

### `playgroundForAgent` 函数

```typescript
import { playgroundForAgent } from '@midscene/playground';
```

`playgroundForAgent` 函数用于为特定的 Agent 创建一个 Playground 启动器，让你可以在浏览器中测试和调试你的自定义界面 Agent。

#### 函数签名

```typescript
function playgroundForAgent(agent: Agent): {
  launch(options?: LaunchPlaygroundOptions): Promise<LaunchPlaygroundResult>
}
```

#### 参数

- `agent: Agent`：要为其启动 Playground 的 Agent 实例

#### 返回值

返回一个包含 `launch` 方法的对象。

#### `launch` 方法选项

```typescript
interface LaunchPlaygroundOptions {
  /**
   * Playground 服务器端口
   * @default 5800
   */
  port?: number;

  /**
   * 是否自动在浏览器中打开 Playground
   * @default true
   */
  openBrowser?: boolean;

  /**
   * 自定义浏览器打开命令
   * @default macOS 使用 'open'，Windows 使用 'start'，Linux 使用 'xdg-open'
   */
  browserCommand?: string;

  /**
   * 是否显示服务器日志
   * @default true
   */
  verbose?: boolean;

  /**
   * Playground 服务器实例的唯一标识 ID
   * 同一个 ID 共用 Playground 对话历史
   * @default undefined（生成随机 UUID）
   */
  id?: string;
}
```

#### `launch` 方法返回值

```typescript
interface LaunchPlaygroundResult {
  /**
   * Playground 服务器实例
   */
  server: PlaygroundServer;

  /**
   * 服务器端口
   */
  port: number;

  /**
   * 服务器主机地址
   */
  host: string;

  /**
   * 关闭 Playground 的函数
   */
  close: () => Promise<void>;
}
```

#### 使用示例

```typescript
import 'dotenv/config';
import { playgroundForAgent } from '@midscene/playground';
import { SampleDevice } from './sample-device';
import { Agent } from '@midscene/core/agent';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 创建设备和 Agent 实例
const device = new SampleDevice();
const agent = new Agent(device);

// 启动 Playground
const result = await playgroundForAgent(agent).launch({
  port: 5800,
  openBrowser: true,
  verbose: true
});

console.log(`Playground 已启动：http://${result.host}:${result.port}`);

// 在需要时关闭 Playground
await sleep(10 * 60 * 1000); // 等待 10 分钟
await result.close();
console.log('Playground 已关闭！');
```

## 常见问题（FAQ）

**我可以使用普通的 LLM 模型（如 GPT-4o）来控制界面吗？**

不可以，你不能使用普通的 LLM 模型（如 GPT-4o）来控制界面。你必须使用具备视觉定位能力的模型。具备视觉定位能力的模型可以在页面上定位目标元素并返回元素的坐标，这能显著提升自动化的稳定性。

请阅读文档以[选择合适的模型](./choose-a-model)。

**我的 interface-controller 可以在本文档中被推荐吗？**

可以，我们很乐意收集有创意的项目并将它们列在本文档中。

当项目准备好后，[给我们提一个 issue](https://github.com/web-infra-dev/midscene/issues)。
